/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.exalm.tabletkat.statusbar; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.res.XModuleResources; import android.content.res.XResources; import android.os.Handler; import android.os.IBinder; import android.os.Parcelable; import android.os.UserHandle; import android.service.notification.StatusBarNotification; import android.text.TextUtils; import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.widget.RemoteViews; import org.exalm.tabletkat.IMod; import org.exalm.tabletkat.TabletKatModule; import org.exalm.tabletkat.statusbar.tablet.StatusBarPanel; import de.robv.android.xposed.XC_MethodReplacement; import de.robv.android.xposed.XposedHelpers; public class BaseStatusBarMod implements IMod { public static final boolean DEBUG = false; public static final String TAG = "BaseStatusBarMod"; public static final boolean MULTIUSER_DEBUG = false; protected static Object self; protected Context mContext; public static final int EXPANDED_LEAVE_ALONE = -10000; protected static final int MSG_TOGGLE_RECENTS_PANEL = 1020; public static final int MSG_CLOSE_RECENTS_PANEL = 1021; protected static final int MSG_OPEN_SEARCH_PANEL = 1024; protected static final int MSG_CLOSE_SEARCH_PANEL = 1025; protected static final int MSG_SHOW_HEADS_UP = 1026; protected static final int MSG_HIDE_HEADS_UP = 1027; protected static final int MSG_ESCALATE_HEADS_UP = 1028; protected static boolean ENABLE_HEADS_UP = true; public static final String SYSTEM_DIALOG_REASON_RECENT_APPS = "recentapps"; protected static final String SETTING_HEADS_UP = "heads_up_enabled"; protected Object mNotificationData; protected WindowManager mWindowManager; protected Handler mHandler; protected View.OnTouchListener mRecentsPreloadOnTouchListener; protected ViewGroup mPile; protected Object mWindowManagerService; protected Object mBarService; protected Object mCommandQueue; protected boolean mIsTv; public void init(Object statusBar){ self = statusBar; mIsTv = TabletKatModule.mTvStatusBarClass.isInstance(self); mContext = (Context) XposedHelpers.getObjectField(self, "mContext"); } public void reset(){ mNotificationData = null; mWindowManager = null; mHandler = null; mRecentsPreloadOnTouchListener = null; mPile = null; mWindowManagerService = null; mBarService = null; mCommandQueue = null; mContext = null; self = null; mIsTv = false; } @Override public void addHooks(ClassLoader cl) { Class tv = TabletKatModule.mTvStatusBarClass; Class base = TabletKatModule.mBaseStatusBarClass; XposedHelpers.findAndHookMethod(tv, "toggleRecentApps", new XC_MethodReplacement() { @Override protected Object replaceHookedMethod(MethodHookParam methodHookParam) throws Throwable { int msg = MSG_TOGGLE_RECENTS_PANEL; mHandler.removeMessages(msg); mHandler.sendEmptyMessage(msg); return null; } }); XposedHelpers.findAndHookMethod(tv, "updateNotification", IBinder.class, StatusBarNotification.class, new XC_MethodReplacement() { @Override protected Object replaceHookedMethod(MethodHookParam methodHookParam) throws Throwable { if (!mIsTv || self == null) { return null; } IBinder key = (IBinder)methodHookParam.args[0]; StatusBarNotification notification = (StatusBarNotification)methodHookParam.args[1]; Object mInterruptingNotificationEntry = XposedHelpers.getObjectField(self, "mInterruptingNotificationEntry"); mRecentsPreloadOnTouchListener = (View.OnTouchListener) XposedHelpers.getObjectField(self, "mRecentsPreloadOnTouchListener"); mPile = (ViewGroup) XposedHelpers.getObjectField(self, "mPile"); if (DEBUG) Log.d(TAG, "updateNotification(" + key + " -> " + notification + ")"); final Object oldEntry = XposedHelpers.callMethod(mNotificationData, "findByKey", key); if (oldEntry == null) { Log.w(TAG, "updateNotification for unknown key: " + key); return null; } final StatusBarNotification oldNotification = (StatusBarNotification)XposedHelpers.getObjectField(oldEntry, "notification"); // XXX: modify when we do something more intelligent with the two content views final RemoteViews oldContentView = oldNotification.getNotification().contentView; final RemoteViews contentView = notification.getNotification().contentView; final RemoteViews oldBigContentView = oldNotification.getNotification().bigContentView; final RemoteViews bigContentView = notification.getNotification().bigContentView; Object row = XposedHelpers.getObjectField(oldEntry, "row"); if (DEBUG) { Object parent = XposedHelpers.callMethod(row, "getParent"); Log.d(TAG, "old notification: when=" + oldNotification.getNotification().when + " ongoing=" + oldNotification.isOngoing() + " expanded=" + XposedHelpers.getObjectField(oldEntry, "expanded") + " contentView=" + oldContentView + " bigContentView=" + oldBigContentView + " rowParent=" + parent); Log.d(TAG, "new notification: when=" + notification.getNotification().when + " ongoing=" + oldNotification.isOngoing() + " contentView=" + contentView + " bigContentView=" + bigContentView); } // Can we just reapply the RemoteViews in place? If when didn't change, the order // didn't change. // 1U is never null boolean contentsUnchanged = (XposedHelpers.getObjectField(oldEntry, "expanded") != null && contentView.getPackage() != null && oldContentView.getPackage() != null && oldContentView.getPackage().equals(contentView.getPackage()) && oldContentView.getLayoutId() == contentView.getLayoutId()); // large view may be null boolean bigContentsUnchanged = (XposedHelpers.callMethod(oldEntry, "getBigContentView") == null && bigContentView == null) || ((XposedHelpers.callMethod(oldEntry, "getBigContentView") != null && bigContentView != null) && bigContentView.getPackage() != null && oldBigContentView.getPackage() != null && oldBigContentView.getPackage().equals(bigContentView.getPackage()) && oldBigContentView.getLayoutId() == bigContentView.getLayoutId()); ViewGroup rowParent = (ViewGroup) XposedHelpers.callMethod(row, "getParent"); int notificationScore = (Integer) XposedHelpers.callMethod(notification, "getScore"); int oldNotificationScore = (Integer) XposedHelpers.callMethod(oldNotification, "getScore"); boolean orderUnchanged = notification.getNotification().when== oldNotification.getNotification().when && notificationScore == oldNotificationScore; // score now encompasses/supersedes isOngoing() StatusBarNotification s = (StatusBarNotification) XposedHelpers.getObjectField(oldEntry, "notification"); boolean updateTicker = notification.getNotification().tickerText != null && !TextUtils.equals(notification.getNotification().tickerText, s.getNotification().tickerText); boolean isTopAnyway = (Boolean)XposedHelpers.callMethod(self, "isTopNotification", rowParent, oldEntry); if (contentsUnchanged && bigContentsUnchanged && (orderUnchanged || isTopAnyway)) { if (DEBUG) Log.d(TAG, "reusing notification for key: " + key); XposedHelpers.setObjectField(oldEntry, "notification", notification); try { updateNotificationViews(oldEntry, notification); if (ENABLE_HEADS_UP && mInterruptingNotificationEntry != null && oldNotification == XposedHelpers.getObjectField(mInterruptingNotificationEntry, "notification")) { if (!(Boolean) XposedHelpers.callMethod(self, "shouldInterrupt", notification)) { if (DEBUG) Log.d(TAG, "no longer interrupts!"); mHandler.sendEmptyMessage(MSG_HIDE_HEADS_UP); } else { if (DEBUG) Log.d(TAG, "updating the current heads up:" + notification); XposedHelpers.setObjectField(mInterruptingNotificationEntry, "notification", notification); updateNotificationViews(mInterruptingNotificationEntry, notification); } } // Update the icon. final Parcelable ic = (Parcelable) XposedHelpers.newInstance(TabletKatModule.mStatusBarIconClass, notification.getPackageName(), (UserHandle) XposedHelpers.callMethod(notification, "getUser"), notification.getNotification().icon, notification.getNotification().iconLevel, notification.getNotification().number, notification.getNotification().tickerText); Object icon = XposedHelpers.getObjectField(oldEntry, "icon"); if (!(Boolean) XposedHelpers.callMethod(icon, "set", ic)) { XposedHelpers.callMethod(self, "handleNotificationError", key, notification, "Couldn't update icon: " + ic); return null; } XposedHelpers.callMethod(self, "updateExpansionStates"); updatePeek(key); } catch (RuntimeException e) { // It failed to add cleanly. Log, and remove the view from the panel. Log.w(TAG, "Couldn't reapply views for package " + contentView.getPackage(), e); removeNotificationViews(key); addNotificationViews(key, notification); } } else { if (DEBUG) Log.d(TAG, "not reusing notification for key: " + key); if (DEBUG) Log.d(TAG, "contents was " + (contentsUnchanged ? "unchanged" : "changed")); if (DEBUG) Log.d(TAG, "order was " + (orderUnchanged ? "unchanged" : "changed")); if (DEBUG) Log.d(TAG, "notification is " + (isTopAnyway ? "top" : "not top")); final boolean wasExpanded = (Boolean) XposedHelpers.callMethod(row, "isUserExpanded"); removeNotificationViews(key); addNotificationViews(key, notification); // will also replace the heads up if (wasExpanded) { final Object newEntry = XposedHelpers.callMethod(mNotificationData, "findByKey", key); Object nRow = XposedHelpers.getObjectField(newEntry, "row"); XposedHelpers.callMethod(nRow, "setExpanded", true); XposedHelpers.callMethod(nRow, "setUserExpanded", true); } } // Update the veto button accordingly (and as a result, whether this row is // swipe-dismissable) XposedHelpers.callMethod(self, "updateNotificationVetoButton", row, notification); // Is this for you? boolean isForCurrentUser = (Boolean)XposedHelpers.callMethod(self, "notificationIsForCurrentUser", notification); if (DEBUG) Log.d(TAG, "notification is " + (isForCurrentUser ? "" : "not ") + "for you"); // Restart the ticker if it's still running if (updateTicker && isForCurrentUser) { XposedHelpers.callMethod(self, "haltTicker"); XposedHelpers.callMethod(self, "tick", key, notification, false); } // Recalculate the position of the sliding windows and the titles. setAreThereNotifications(); XposedHelpers.callMethod(self, "updateExpandedViewPos", EXPANDED_LEAVE_ALONE); return null; } }); //AOSPA hook try { XposedHelpers.findAndHookMethod(base, "updateHoverState", XC_MethodReplacement.DO_NOTHING); }catch (NoSuchMethodError e){} } protected void updatePeek(IBinder key) { } public void onStart() { if (!mIsTv){ return; } mHandler = (Handler) XposedHelpers.getObjectField(self, "mHandler"); mRecentsPreloadOnTouchListener = (View.OnTouchListener) XposedHelpers.getObjectField(self, "mRecentsPreloadOnTouchListener"); mPile = (ViewGroup) XposedHelpers.getObjectField(self, "mPile"); mBarService = XposedHelpers.getObjectField(self, "mBarService"); mCommandQueue = XposedHelpers.getObjectField(self, "mCommandQueue"); } @Override public void initResources(XResources res, XModuleResources res2) { } protected boolean inKeyguardRestrictedInputMode() { try { return (Boolean) XposedHelpers.callMethod(self, "inKeyguardRestrictedInputMode"); }catch (NoSuchMethodError e){ return false; } } public void toggleRecentApps() { XposedHelpers.callMethod(self, "toggleRecentApps"); } protected void updateSearchPanel() { try { XposedHelpers.callMethod(self, "updateSearchPanel"); }catch(NoSuchMethodError e){} } protected void showSearchPanel() { XposedHelpers.callMethod(self, "showSearchPanel"); } public boolean inflateViews(Object entry, ViewGroup parent) { return (Boolean) XposedHelpers.callMethod(self, "inflateViews", entry, parent); } protected void visibilityChanged(boolean visible) { XposedHelpers.callMethod(self, "visibilityChanged", visible); } protected View.OnLongClickListener getNotificationLongClicker() { return (View.OnLongClickListener) XposedHelpers.callMethod(self, "getNotificationLongClicker"); } protected Object addNotificationViews(IBinder key, StatusBarNotification notification) { Object o = XposedHelpers.callMethod(self, "createNotificationViews", key, notification); addNotificationViews(o); return o; } protected void addNotificationViews(Object o) { XposedHelpers.callMethod(self, "addNotificationViews", o); } protected StatusBarNotification removeNotificationViews(IBinder key) { return (StatusBarNotification) XposedHelpers.callMethod(self, "removeNotificationViews", key); } protected void tick(IBinder key, StatusBarNotification n, boolean firstTime){ XposedHelpers.callMethod(self, "tick", key, n, firstTime); } public void setSystemUiVisibility(int vis, int mask) { XposedHelpers.callMethod(self, "setSystemUiVisibility", vis, mask); } public boolean isDeviceProvisioned() { return (Boolean) XposedHelpers.callMethod(self, "isDeviceProvisioned"); } protected void setAreThereNotifications() { XposedHelpers.callMethod(self, "setAreThereNotifications"); } protected boolean showNotificationEvenIfUnprovisioned(StatusBarNotification sbn) { return (Boolean) XposedHelpers.callMethod(self, "showNotificationEvenIfUnprovisioned", sbn); } protected void updateNotificationIcons(){ XposedHelpers.callMethod(self, "updateNotificationIcons"); } protected boolean shouldDisableNavbarGestures(){ return (Boolean) XposedHelpers.callMethod(self, "shouldDisableNavbarGestures"); } private void updateNotificationViews(Object entry, StatusBarNotification notification) { View expanded = (View) XposedHelpers.getObjectField(entry, "expanded"); View eBigContentView = (View) XposedHelpers.callMethod(entry, "getBigContentView"); View content = (View) XposedHelpers.getObjectField(entry, "content"); final RemoteViews contentView = notification.getNotification().contentView; final RemoteViews bigContentView = notification.getNotification().bigContentView; // Reapply the RemoteViews contentView.reapply(mContext, expanded, mOnClickHandler); if (bigContentView != null && eBigContentView != null) { bigContentView.reapply(mContext, eBigContentView, mOnClickHandler); } // update the contentIntent final PendingIntent contentIntent = notification.getNotification().contentIntent; if (contentIntent != null) { final View.OnClickListener listener = makeClicker(contentIntent, notification.getPackageName(), notification.getTag(), notification.getId()); content.setOnClickListener(listener); } else { content.setOnClickListener(null); } } public View.OnClickListener makeClicker(PendingIntent p, String s, String tag, int id) { return (View.OnClickListener) XposedHelpers.callMethod(self, "makeClicker", p, s, tag, id); } private RemoteViews.OnClickHandler mOnClickHandler = new RemoteViews.OnClickHandler() { @Override public boolean onClickHandler(View view, PendingIntent pendingIntent, Intent fillInIntent) { if (DEBUG) { Log.v(TAG, "Notification click handler invoked for intent: " + pendingIntent); } final boolean isActivity = (Boolean) XposedHelpers.callMethod(pendingIntent, "isActivity"); if (isActivity) { try { Object o = XposedHelpers.callStaticMethod(TabletKatModule.mActivityManagerNativeClass, "getDefault"); XposedHelpers.callMethod(o, "resumeAppSwitches"); XposedHelpers.callMethod(o, "dismissKeyguardOnNextActivity"); } catch (Exception e) { } } boolean handled = super.onClickHandler(view, pendingIntent, fillInIntent); if (isActivity && handled) { // close the shade if it was open animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE); visibilityChanged(false); } return handled; } }; public void animateCollapsePanels(int i) { XposedHelpers.callMethod(self, "animateCollapsePanels", i); } protected boolean shouldInterrupt(StatusBarNotification sbn) { return (Boolean) XposedHelpers.callMethod(self, "shouldInterrupt", sbn); } public void resetHeadsUpDecayTimer() { try { XposedHelpers.callMethod(self, "resetHeadsUpDecayTimer"); } catch (NoSuchMethodError e) { ENABLE_HEADS_UP = false; } } protected void notifyHeadsUpScreenOn(boolean screenOn) { try { XposedHelpers.callMethod(self, "notifyHeadsUpScreenOn", screenOn); } catch (NoSuchMethodError e) { ENABLE_HEADS_UP = false; } } protected boolean notificationIsForCurrentUser(StatusBarNotification n) { return (Boolean) XposedHelpers.callMethod(self, "notificationIsForCurrentUser", n); } protected WindowManager.LayoutParams getSearchLayoutParams(ViewGroup.LayoutParams layoutParams) { try { return (WindowManager.LayoutParams) XposedHelpers.callMethod(self, "getSearchLayoutParams", layoutParams); } catch (NoSuchMethodError e) { return null; } } public class TouchOutsideListener implements View.OnTouchListener { private int mMsg; private Object mPanel; public TouchOutsideListener(int msg, Object panel) { mMsg = msg; mPanel = panel; } public boolean onTouch(View v, MotionEvent ev) { final int action = ev.getAction(); if (action == MotionEvent.ACTION_OUTSIDE || (action == MotionEvent.ACTION_DOWN && !(Boolean) XposedHelpers.callMethod(mPanel, "isInContentArea", (int)ev.getX(), (int)ev.getY()))) { mHandler.removeMessages(mMsg); mHandler.sendEmptyMessage(mMsg); return true; } return false; } } }